require "svglib"

class Spectrum

  # An internal class used to combine mass-intensity pairs.
  Peak = Struct.new(:mass, :intensity)

  def initialize(job)
    @peaks = parse_ms_file(job.ms_file)
    @intensity_treshold = job.intensity_threshold
    @selected_peaks = job.fragments.selected.map(&:peaknum)
  end

  def to_svg(options = {})
    options.reverse_merge!(:width => 880, :height => 300,
                           :padding => 40, :scale_factor => 1.05)
    @width         = options[:width].to_i
    @height        = options[:height].to_i
    @padding       = options[:padding].to_i
    @scale_factor  = options[:scale_factor].to_f
    @max_intensity = @peaks.map(&:intensity).max * @scale_factor
    @max_mass      = @peaks.map(&:mass).max * @scale_factor

    image = SvgImage.new(:viewBox => "0 0 #{@width} #{@height}") do |canvas|
      canvas.g(:stroke => "black", "stroke-width" => 1, :fill => "none",
               "font-size" => [@height / 15, 11].min, :style => "text-anchor: middle",
               "font-family" => "Helvetica, Arial, sans-serif") do

        # Draw a rectangle that acts as the image borders.
        canvas.rect(:x => 0, :y => 0, :width => "100%", :height => "100%")
        # Draw the spectrum axes.
        canvas.path(:d => "M #{@padding} 0 L #{@padding} #{@height - @padding}
                    L #{@width} #{@height - @padding}")

        # Draw the axis descriptions.
        canvas.g(:stroke => "none", :fill => "gray") do
          x, y = @padding * 0.25, (@height - @padding) / 2
          canvas.text("Intensity", :x => x, :y => y,
                      :transform => "rotate(-90, #{x}, #{y})")
          x, y = (@width + @padding) / 2, @height - @padding * 0.1
          canvas.text("m/z", :x => x, :y => y)
        end

        # Draw the spectrum y-axis (intensity) markers.
        (100 .. @max_intensity - 0.1).step(100).each do |intensity|
          y = transform_intensity(intensity)
          canvas.line(:x1 => @padding - (@padding * 0.1), :y1 => y,
                      :x2 => @padding + (@padding * 0.1), :y2 => y)
          canvas.text(intensity.to_i, :x => @padding * 0.85, :y => y,
                      :dy => "0.35em", :fill => "black", :stroke => "none",
                      "text-anchor" => "end")
        end

        # Draw the spectrum x-axis (m/z) markers.
        (50 .. @max_mass - 0.1).step(50).each do |mass|
          x = transform_mass(mass)
          canvas.line(:x1 => x, :y1 => @height - @padding - (@padding * 0.1),
                      :x2 => x, :y2 => @height - @padding + (@padding * 0.1))
          canvas.text(mass.to_i, :x => x, :y => @height - @padding * 0.85,
                      :fill => "black", :stroke => "none", :dy => "0.75em")
        end

        # Draw the intensity threshold line.
        y = transform_intensity(@intensity_treshold)
        canvas.line(:x1 => transform_mass(0), :y1 => y, :x2 => transform_mass(@max_mass), :y2 => y)
        canvas.text("Intensity treshold", :x => transform_mass(0), :y => y, :fill => "black", :stroke => "none",
                    :style => "text-anchor: start", :dx => 5, :dy => -3)

        # Draw the peaks.
        @peaks.each_with_index do |peak, peaknum|
          x, y = transform_mass(peak.mass), transform_intensity(peak.intensity)
          color = @selected_peaks.include?(peaknum) ? "green" : "#BF211E"
          canvas.line(:x1 => x, :y1 => transform_intensity(0), :x2 => x,
                      :y2 => y, :stroke => color, "stroke-width" => 3)
        end
      end
    end
    image.to_svg
  end

  private

  # Parses the MS file. Returns an array of mass-intensity pairs.
  def parse_ms_file(ms_file)
    ms_file.each_line.map do |line|
      mass, intensity = line.split(/\s+/)
      Peak.new(mass.to_f, intensity.to_f)
    end
  end

  # Transform the given mass to the corresponding x-coordinate on the canvas.
  def transform_mass(mass)
    mass / @max_mass * (@width - @padding) + @padding
  end

  # Transform the given intensity to the corresponding y-coordinate on the
  # canvas.
  def transform_intensity(intensity)
    @height - @padding - ((intensity.to_f / @max_intensity) * (@height - @padding))
  end
end
